﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net;
using System.Collections.Specialized;

namespace FHJ.FHPI.PushNotifications.Library.GoogleC2DM
{
    public static class MessageProcessor
    {
        /// <summary>
        /// Background worker which processes the sending of C2DM messages.
        /// </summary>
        public class BackgroundWorker
        {
            private Thread myThread;
            private Object myThreadSyncRoot = new Object();

            private volatile Boolean stopRequested;

            private Queue<Types.C2DMProcessMessage> messagesOpenToProcess;
            private Object messagesOpenToProcessSyncRoot = new Object();

            private const Int32 MAX_CONCURRENT_MESSAGES_TO_PROCESS = 1;
            private List<Types.C2DMProcessMessage> messagesInProcess;
            private Dictionary<Types.C2DMProcessMessage, DateTime> messagesInProcessRequestStartTimes;
            private Object messagesInProcessSyncRoot = new Object();

            private DateTime? googleServiceRetryNextTimestamp = null;
            private Int32? lastRetryAddTime = null;
            private const Int32 RETRY_DEFAULT_BACKOFF_SEC = 60;
            private DateTime? lastErrorTryTimestamp = null;

            private const String ERROR_CODE_INVALID_REGISTRATION = "InvalidRegistration";
            private const String ERROR_CODE_NOTREGISTERED = "NotRegistered";

            /// <summary>
            /// Starts the processing loop in a separate Thread.
            /// </summary>
            public void Start()
            {
                try
                {
                    lock (myThreadSyncRoot)
                    {
                        if (IsRunning())
                            return;

                        myThread = new Thread(new ThreadStart(this.Run));
                        myThread.IsBackground = true;
                        myThread.Start();
                    }
                }
                catch (Exception)
                {                    
                    throw;
                }
            }

            /// <summary>
            /// Indicates whether the background processing is currently running.
            /// </summary>
            public Boolean IsRunning()
            {
                try
                {
                    lock (myThreadSyncRoot)
                    {
                        if (myThread == null)
                            return false;
                        else
                            return myThread.IsAlive;
                    }
                }
                catch (Exception)
                {                    
                    throw;
                }
            }

            /// <summary>
            /// Stops the requests.
            /// </summary>
            public void Stop()
            {
                try
                {
                    stopRequested = true;
                }
                catch (Exception)
                {                    
                    throw;
                }
            }

            /// <summary>
            /// Loop method which does the work of processing the messages.
            /// </summary>
            public void Run()
            {
                try
                {
                    lock (messagesOpenToProcessSyncRoot)
                    {
                        messagesOpenToProcess = new Queue<Types.C2DMProcessMessage>();
                    }
                    lock (messagesInProcessSyncRoot)
                    {
                        messagesInProcess = new List<Types.C2DMProcessMessage>();
                        messagesInProcessRequestStartTimes = new Dictionary<Types.C2DMProcessMessage, DateTime>();
                    }

                    while (!stopRequested)
                    {
                        #region Get new messages...
                        try
                        {
                            List<Types.C2DMProcessMessage> newMessagesTemp = DbWrapper.GetListOfOpenMessages_C2DM();

                            lock (messagesOpenToProcessSyncRoot)
                            {
                            lock (messagesInProcessSyncRoot)
                            {
                                    foreach (var newMessageTemp in newMessagesTemp)
                                    {
                                        // Check if the item is already in one of our lists, do not add duplicates...
                                        if (messagesOpenToProcess.Count(item => item.PushMessage.Id == newMessageTemp.PushMessage.Id && item.DeviceRegistrationId == newMessageTemp.DeviceRegistrationId) <= 0 &&
                                            messagesInProcess.Count(item => item.PushMessage.Id == newMessageTemp.PushMessage.Id && item.DeviceRegistrationId == newMessageTemp.DeviceRegistrationId) <= 0)
                                                messagesOpenToProcess.Enqueue(newMessageTemp);
                                    }
                            }
                            }
                        }
                        catch (MySql.Data.MySqlClient.MySqlException)
                        {
                            // Ignore MySql connections (server unavailable etc.)
                            // --> When the server is available again we continue to work...
                            // ToDo: Log
                        }
                        catch (Exception)
                        {
                            throw;
                        }
                        #endregion

                        #region Start requests for the items...
                        lock (messagesOpenToProcessSyncRoot)
                        {                        
                            String authToken;
                            Boolean authTokenValid;
                            AuthProvider.Instance.GetAuthToken(out authTokenValid, out authToken);                       
                            
                            while (
                                   (messagesInProcess.Count < MAX_CONCURRENT_MESSAGES_TO_PROCESS) &&
                                   (messagesOpenToProcess.Count > 0) &&
                                   (!googleServiceRetryNextTimestamp.HasValue || googleServiceRetryNextTimestamp < DateTime.Now)
                                  )
                            {
                                Types.C2DMProcessMessage currentQueueMessage = messagesOpenToProcess.Dequeue();

                                // Add to processing list...
                                lock (messagesInProcessSyncRoot)
                                {
                                    messagesInProcess.Add(currentQueueMessage);
                                    messagesInProcessRequestStartTimes.Add(currentQueueMessage, DateTime.Now);
                                }

                                // Start request...
                                RequestHandler.DoSendMessageRequestAsync(
                                    currentQueueMessage,
                                    currentQueueMessage.DeviceRegistrationId,
                                    authToken,
                                    currentQueueMessage.PushMessage.Type,
                                    currentQueueMessage.PushMessage.AdditionalInfo,
                                    new UploadValuesCompletedEventHandler(this.RequestCallback));
                            }
                        }
                        #endregion

                        // Sleep between the loop runs...
                        if (!stopRequested)
                            Thread.Sleep(100);
                    }

                    // Wait until the already running processes have finished...
                    while (messagesInProcess.Count > 0)
                    {
                        System.Threading.Thread.Sleep(100);
                    }
                }
                catch (Exception)
                {
                    throw;
                }
                finally
                {
                    lock (myThreadSyncRoot)
                    {
                        myThread = null;
                    }
                    stopRequested = false;
                }
            }

            /// <summary>
            /// Is called (as callback) when a request is completed. (The requests are made asynchronous).
            /// </summary>
            private void RequestCallback(Object sender, UploadValuesCompletedEventArgs e)
            {
                Types.C2DMProcessMessage thisRequestMessage = e.UserState as Types.C2DMProcessMessage;

                try
                {                    
                    if (e.Error == null)
                    {
                        if (googleServiceRetryNextTimestamp.HasValue && googleServiceRetryNextTimestamp.Value > DateTime.Now)
                        {
                            // Currently there's an wait time set to wait until the next requests are made
                            // (due to service unavailable errors)

                            // If this request has been made after the last request with error,
                            // the service works again and we can continue to send.

                            if (messagesInProcessRequestStartTimes.ContainsKey(thisRequestMessage) &&
                                lastErrorTryTimestamp.HasValue &&
                                messagesInProcessRequestStartTimes[thisRequestMessage] > lastErrorTryTimestamp.Value)
                            {
                                googleServiceRetryNextTimestamp = null;
                                lastRetryAddTime = null;
                                lastErrorTryTimestamp = null;
                            }
                        }

                        // Request worked fine...
                        NameValueCollection responseValues = RequestHandler.ParseC2DMRequestResponse(e.Result);
                        
                        String messageId = "<NULL>";
                        if (responseValues.AllKeys.Contains("id") && !(responseValues["id"] == null))
                            messageId = responseValues["id"];

                        if (responseValues.AllKeys.Contains("Error"))
                        {
                            String errorCode = "<NULL>";
                            if (responseValues.AllKeys.Contains("Error") && responseValues["Error"] == null)
                                errorCode = responseValues["Error"];

                            thisRequestMessage.Success = false;
                            thisRequestMessage.StatusMessage = String.Format(Properties.Resources.Res_000003, messageId, errorCode);

                            // Delete the device's registration from our database
                            // (see https://developers.google.com/android/c2dm/#push)
                            // * InvalidRegistration — Missing or bad registration_id. Sender should stop sending messages to this device.
                            // * NotRegistered — The registration_id is no longer valid, for example user has uninstalled the application or turned off notifications.
                            //                   Sender should stop sending messages to this device. 
                            if (String.Compare(ERROR_CODE_INVALID_REGISTRATION, errorCode, true) == 0 ||
                                String.Compare(ERROR_CODE_NOTREGISTERED, errorCode, true) == 0)
                            {
                                DbWrapper.DeleteC2DMClient(thisRequestMessage.DeviceRegistrationId);
                            }
                        }
                        else
                        {
                            thisRequestMessage.Completed = true;
                            thisRequestMessage.Success = true;
                            thisRequestMessage.StatusMessage = String.Format(Properties.Resources.Res_000002, messageId);
                        }

                        DbWrapper.UpdateMessageItem_C2DM(thisRequestMessage);                        
                    }
                    else
                    {
                        // Check the errors
                        if (!(e.Error == null) && (e.Error is WebException) && !(((WebException)e.Error).Response == null))
                        {
                            WebException we = (WebException)e.Error;
                            HttpWebResponse response = (System.Net.HttpWebResponse)we.Response;
                            Int32 responseCode = (Int32)response.StatusCode;

                            #region Response 503 (ServiceUnavailable)
                            if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
                            {
                                // see https://developers.google.com/android/c2dm/#push
                                // Indicates that the server is temporarily unavailable (i.e., because of timeouts, etc ).
                                // Sender must retry later, honoring any Retry-After header included in the response.
                                // Application servers must implement exponential back off. Senders that create problems risk being blacklisted.

                                lastErrorTryTimestamp = DateTime.Now;
                                if (messagesInProcessRequestStartTimes.ContainsKey(thisRequestMessage))
                                    lastErrorTryTimestamp = messagesInProcessRequestStartTimes[thisRequestMessage];

                                // Try to parse the Retry-After header (if exists)...
                                DateTime? retryAfterHeader = RequestHandler.ParseRetryAfterHeader(response);

                                // If the header is specified, set our timestamp to this time...
                                if (!(retryAfterHeader == null))
                                {
                                    googleServiceRetryNextTimestamp = retryAfterHeader;
                                }
                                else
                                {
                                    if (lastRetryAddTime == null)
                                        lastRetryAddTime = RETRY_DEFAULT_BACKOFF_SEC;
                                    else
                                        lastRetryAddTime *= 2;

                                    googleServiceRetryNextTimestamp = DateTime.Now.AddSeconds(lastRetryAddTime.Value);
                                }
                             
                                // Enqueue message again...
                                lock (messagesOpenToProcessSyncRoot)
                                {
                                    messagesOpenToProcess.Enqueue(thisRequestMessage);
                                }
                            }
                            #endregion
                            #region Response 401 (Unauthorized)
                            else if (response.StatusCode == HttpStatusCode.Unauthorized)
                            {
                                // see https://developers.google.com/android/c2dm/#push
                                // Indicates that the ClientLogin AUTH_TOKEN used to validate the sender is invalid.

                                AuthProvider.Instance.RefreshAuthToken();

                                // Enqueue message again...
                                lock (messagesOpenToProcessSyncRoot)
                                {
                                    messagesOpenToProcess.Enqueue(thisRequestMessage);
                                }
                            }
                            #endregion
                        }
                        else
                        {
                            thisRequestMessage.Completed = true;
                            thisRequestMessage.Success = false;
                            thisRequestMessage.StatusMessage = e.Error.Message;

                            DbWrapper.UpdateMessageItem_C2DM(thisRequestMessage);
                        }
                    }
                }
                catch (Exception)
                {
                    // Ignore errors here...
                    // ToDo: Log
                }
                finally
                {
                    lock (messagesInProcessSyncRoot)
                    {
                        if (!(thisRequestMessage == null) && messagesInProcess.Contains(thisRequestMessage))
                            messagesInProcess.Remove(thisRequestMessage);
                        if (!(thisRequestMessage == null) && messagesInProcessRequestStartTimes.ContainsKey(thisRequestMessage))
                            messagesInProcessRequestStartTimes.Remove(thisRequestMessage);
                    }
                }
            }
        }

        /// <summary>
        /// Static initializer.
        /// </summary>
        static MessageProcessor()
        {
            myBackgroundWorker = new BackgroundWorker();
        }
        
        /// <summary>
        /// The background worker which in fact does the processing.
        /// </summary>
        private static BackgroundWorker myBackgroundWorker
        {
            get;
            set;
        }

        /// <summary>
        /// Starts the processing of messages.
        /// </summary>
        public static void Start()
        {
            try
            {
                myBackgroundWorker.Start();
            }
            catch (Exception)
            {                
                throw;
            }
        }

        /// <summary>
        /// Stops the processing of messages. Method blocks until all running background threads currently processing messages have stopped.
        /// </summary>
        public static void Stop()
        {
            try
            {
                myBackgroundWorker.Stop();

                while (myBackgroundWorker.IsRunning())
                {
                    Thread.Sleep(100);
                }
            }
            catch (Exception)
            {                
                throw;
            }
        }
    }
}
